package com.screwdriver.jso.processor.utils;

import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.declaration.Declaration;
import com.sun.mirror.declaration.InterfaceDeclaration;
import com.sun.mirror.declaration.MethodDeclaration;
import com.sun.mirror.declaration.TypeParameterDeclaration;
import com.sun.mirror.type.*;

import java.math.BigDecimal;
import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: jan kowalski
 * Date: 21.08.11
 * Time: 14:31
 * To change this template use File | Settings | File Templates.
 */
public class TypeResolver {

    public static enum Type {

        VOID(false, true),
        LONG(true, false), INT(true, false), BYTE(true, false), SHORT(true, false), FLOAT(true, false), BOOLEAN(true, false), DOUBLE(true, false), CHAR(true, false),
        STRING(false, false),
        ENUM(false, false),
        DATE(false, false), BIGDECIMAL(false, false),
        JSO(false, false),
        COLLECTION(false, false), LIST(false, false), SET(false, false), MAP(false, false);

        private boolean primitive;
        private boolean _void;

        Type(boolean primitive, boolean _void) {
            this.primitive = primitive;
            this._void = _void;
        }
    }

    public static class TypeTouple {

        private final Type type;
        private final DeclaredType declaredType;
        private final boolean isArray;

        TypeTouple(Type type, DeclaredType declaredType) {
            this(type, declaredType, false);
        }

        TypeTouple(Type type, DeclaredType declaredType, boolean isArray) {
            this.type = type;
            this.declaredType = declaredType;
            this.isArray = isArray;
        }

        public Type getType() {
            return type;
        }

        public DeclaredType getDeclaredType() {
            return declaredType;
        }

        public boolean isArray() {
            return isArray;
        }

    }

    private List<TypeResolver> declaredTypeArguments = new LinkedList<TypeResolver>();
    private AnnotationProcessorEnvironment environment;
    private InterfaceDeclaration jsoInterface;
    private InterfaceType declaringSuperInterface;

    private TypeTouple rootType;

    public TypeResolver(AnnotationProcessorEnvironment environment, InterfaceDeclaration jsoInterface, InterfaceType declaringSuperInterface, TypeMirror returnType) {
        this.environment = environment;
        this.jsoInterface = jsoInterface;
        this.declaringSuperInterface = declaringSuperInterface;
        environment.getMessager().printNotice(String.format("resolving %s", returnType));

        resolveInvolvedTypes(declaringSuperInterface, returnType);
    }

    public boolean isArray() {
        return rootType.isArray();
    }

    public Type getType() {
        return rootType.getType();
    }

    public DeclaredType getDeclaredType() {
        return rootType.getDeclaredType();
    }

    private void resolveInvolvedTypes(InterfaceType declaringSuperInterface, TypeMirror returnType) {
        rootType = resolveType(declaringSuperInterface, returnType);
        if (!rootType.getType().primitive && !rootType.getType()._void) {
            Collection<TypeMirror> actualTypeArguments = rootType.getDeclaredType().getActualTypeArguments();
            if (actualTypeArguments.size() > 0) {
                environment.getMessager().printNotice(String.format("resolving type parameters of %s", rootType));
                for (TypeMirror rm : actualTypeArguments) {
                    declaredTypeArguments.add(new TypeResolver(environment, jsoInterface, declaringSuperInterface, rm));
                }
            }
        }
    }

    public TypeTouple resolveType(InterfaceType declaringSuperInterface, TypeMirror type) {
        if (type instanceof DeclaredType) {
            DeclaredType declaredType = (DeclaredType) type;
            return new TypeTouple(fromDeclaredType(declaredType), declaredType);
        } else if (type instanceof VoidType) {
            return new TypeTouple(Type.VOID, null);
        } else if (type instanceof PrimitiveType) {
            return new TypeTouple(fromPrimitiveType(type), null);
        } else if (type instanceof ArrayType) {
            TypeTouple resolveType = resolveType(declaringSuperInterface, ((ArrayType) type).getComponentType());
            return new TypeTouple(resolveType.getType(), resolveType.getDeclaredType(), true);
        } else if (type instanceof TypeVariable) {
            TypeVariable typeVariable = (TypeVariable) type;
            if (!(typeVariable.getDeclaration().getOwner() instanceof MethodDeclaration)) {
                return composeTypeVariable(findInterfaceTypeOwningTypeVariable(typeVariable), (TypeVariable) type);
            }
        }
        throw new RuntimeException("unsupported type of method return type: " + type.toString());
    }

    private TypeTouple composeTypeVariable(InterfaceType declaringInterface, TypeVariable type) {
        TypeParameterDeclaration wantedTypeParameterDeclaration = type.getDeclaration();

        if (declaringInterface != null && declaringInterface.getActualTypeArguments() != null) {
            Iterator<TypeMirror> actualTypeArgumentsIterator = declaringInterface.getActualTypeArguments().iterator();
            Iterator<TypeParameterDeclaration> typeParameterDeclarationsIterator = declaringInterface.getDeclaration()
                    .getFormalTypeParameters().iterator();

            while (typeParameterDeclarationsIterator.hasNext() && actualTypeArgumentsIterator.hasNext()) {
                TypeMirror typeArgument = actualTypeArgumentsIterator.next();
                if (typeParameterDeclarationsIterator.next().equals(wantedTypeParameterDeclaration)) {
                    return resolveType(declaringInterface, typeArgument);
                }
            }
        } else {
            environment.getMessager().printError("" + declaringInterface);
        }

        throw new RuntimeException("unsupported type of method return type: " + type.toString());
    }

    private Type fromDeclaredType(DeclaredType type) {
        if (type instanceof EnumType) {
            return Type.ENUM;
        } else if (type instanceof InterfaceDeclaration) {
            String qualifiedName = type.getDeclaration().getQualifiedName();
            if (List.class.getCanonicalName().equals(qualifiedName)) {
                return Type.LIST;
            } else if (Set.class.getCanonicalName().equals(qualifiedName)) {
                return Type.SET;
            } else if (Map.class.getCanonicalName().equals(qualifiedName)) {
                return Type.MAP;
            } else if (Collection.class.getCanonicalName().equals(qualifiedName)) {
                return Type.COLLECTION;
            }
        } else if (type instanceof ClassType) {
            String qualifiedName = type.getDeclaration().getQualifiedName();
            if (String.class.getCanonicalName().equals(qualifiedName)) {
                return Type.STRING;
            } else if (BigDecimal.class.getCanonicalName().equals(qualifiedName)) {
                return Type.BIGDECIMAL;
            } else if (Date.class.getCanonicalName().equals(qualifiedName)) {
                return Type.DATE;
            }
        }
        return Type.JSO;
    }

    private Type fromPrimitiveType(TypeMirror type) {
        switch (((PrimitiveType) type).getKind()) {
            case BOOLEAN:
                return Type.BOOLEAN;
            case SHORT:
                return Type.SHORT;
            case BYTE:
                return Type.BYTE;
            case INT:
                return Type.INT;
            case LONG:
                return Type.LONG;
            case FLOAT:
                return Type.FLOAT;
            case DOUBLE:
                return Type.DOUBLE;
            case CHAR:
                return Type.CHAR;
        }
        return null;
    }

    public InterfaceType findInterfaceTypeOwningTypeVariable(TypeVariable typeVariable) {
        Declaration owner = typeVariable.getDeclaration().getOwner();
        List<InterfaceType> interfaces = new LinkedList<InterfaceType>();
        interfaces.addAll(jsoInterface.getSuperinterfaces());

        while (interfaces.size() > 0) {
            InterfaceType removed = interfaces.remove(0);
            if (removed.getDeclaration().equals(owner)) {
                return removed;
            }
            interfaces.addAll(removed.getSuperinterfaces());
        }

        environment.getMessager().printWarning(
                String.format("couldn't find interfacetype for %s starting from %s", owner.toString(),
                        declaringSuperInterface.getDeclaration().getQualifiedName()));
        return null;
    }

    public String getQualifiedName() {
        if (rootType.getType().primitive) {
            return rootType.getType().name().toLowerCase();
        } else if (rootType.getType()._void) {
            return rootType.getType().name().toLowerCase();
        } else {
            return rootType.getDeclaredType().getDeclaration().getQualifiedName();
        }
    }

    public String getName() {
        if (rootType.getType().primitive) {
            return rootType.getType().name().toLowerCase();
        } else if (rootType.getType()._void) {
            return rootType.getType().name().toLowerCase();
        } else {
            return rootType.getDeclaredType().getDeclaration().getSimpleName();
        }
    }

    public String getQualifiedParameterizedName() {
        String result = "";
        if (rootType.getType()._void) {
            result += rootType.getType().name().toLowerCase();
        } else {
            if (rootType.getType().primitive) {
                result += rootType.getType().name().toLowerCase();
            } else {
                String qualifiedName = rootType.getDeclaredType().getDeclaration().getQualifiedName();
                if (declaredTypeArguments.size() > 0) {
                    qualifiedName += "<";
                    for (int i = 0; i < declaredTypeArguments.size(); i++) {
                        if (i > 0)
                            qualifiedName += ",";
                        qualifiedName += declaredTypeArguments.get(i).getQualifiedParameterizedName();
                    }
                    qualifiedName += ">";
                }
                result += qualifiedName;
            }
             result += (isArray() ? "[]" : "");
        }
        return result;
    }

    public String getParameterizedName() {
        String result = "";
        if (rootType.getType()._void) {
            result += rootType.getType().name().toLowerCase();
        } else {

            if (rootType.getType().primitive) {
                result += rootType.getType().name().toLowerCase();
            } else {
                String simpleName = rootType.getDeclaredType().getDeclaration().getSimpleName();
                if (declaredTypeArguments.size() > 0) {
                    simpleName += "<";
                    for (int i = 0; i < declaredTypeArguments.size(); i++) {
                        if (i > 0)
                            simpleName += ",";
                        simpleName += declaredTypeArguments.get(i).getParameterizedName();
                    }
                    simpleName += ">";
                }
                result += simpleName;
            }
            result += (isArray() ? "[]" : "");
        }
        return result;
    }

    public List<DeclaredType> getInvolvedTypes() {
        List<DeclaredType> result = new LinkedList<DeclaredType>();
        if (!rootType.getType()._void && !rootType.getType().primitive) {
            result.add(rootType.getDeclaredType());
            for (TypeResolver rt : declaredTypeArguments) {
                result.addAll(rt.getInvolvedTypes());
            }
        }
        return result;
    }

}
